1. References and PoC
This bug was found by saelo(Thanks saelo for always being really great!).
https://bugs.chromium.org/p/project-zero/issues/detail?id=1775
https://github.com/WebKit/webkit/commit/62f770031bdb15b59041257e60ab93765d5ee6ca
1 | const v3 = [1337, 1337, 1337, 1337]; |
2. the Crash
This bug crashes at “v8[-698666199]”, which corresponds to a dfg node GetByVal.
Before LICM phase, @70 GetByVal resides in Block#3, with a @137 CheckInBounds ahead of it.
After LICM, it resides in Block#1, with @137 CheckInBounds still in Block#3.
It means that @70 GetByVal is hoisted during LICM. So read operation happens before check. Thus crash happens.
3. the Patch & the Analysis
After implementing the patch, @70 GetByVal is never hoisted because of it won’t pass edgesDominate in DFGLICMPhase.cpp:
1 | if (!edgesDominate(m_graph, node, data.preHeader)) { |
(jsc’s LICM does not strictly implement traditional fundamentals of compiling. It checks doesWrites, readsOverlap, safeToExecute and sort of things. It does not really rely on defs and uses but AbstractHeap.)
And here is the calling stack:
1 | if (!edgesDominate(m_graph, node, data.preHeader)) { |
So at function dominates
,
the vuln version(the index are the blocks’):
to 0 from 0
to 0 from 0
to 0 from 0
the fixed version:
to 0 from 0
to 0 from 0
to 0 from 0
to 0 from 3
m_data[to].preNumber 0 m_data[from].preNumber 3
m_data[to].postNumber 17 m_data[from].postNumber 0
That’s because an Edge is added in the Graph in the fixed version.
1 | before LICM |
So we could know that the core of the patch is as follows:
1 |
|
(The HasIndexedProperty stuff has nothing to do with this bug. It is modified in the patch because in lowerBoundsCheck is modified in DFGSSALowingPhase.cpp because it relies on that function.)
So there is a first one condition that a node should meet if the node would be hoisted during LICM phase:
- All of the nodes’ children should dominates the loop’s preheader.
In this case, Block#0 is definately loop’s preheader. And GetByVal has children:
- base@22
- index@69
- storage@4
Those children all belong to Block#0. And the 4th child in the fixed version:
- @137 CheckInBounds
What makes @137 CheckInBounds the 4th child of GetByVal is an artificial (because there isn’t actually any data dependency) dataflow edge between the CheckInBounds and the GetByVal.
So in the vuln verison, we have Block#0 not-strictly dominate Block#0, and in the vuln version Block#3 not dominate Block#0 which determines the existence of the vulnerability. Thus GetByVal can never be hoisted in front of the check.
(Thanks for saelo’s advice on emphasizing the artificial dataflow edge here. )
4. about CheckInBounds
@137 CheckInBounds is never hoisted. Because before LICM phase(the 42th phase) it is like:
1 | 137:<!0:-> CheckInBounds(Int32:@69, KnownInt32:Kill:@159, MustGen, Int32, Exits, bc#54, ExitValid) |
And @69 resides in Block#0 and @159 resides in Block#2. And Block#2 does not dominate Block#0. So.
There is a @224 CheckInBounds inserted before @70 GetByVal during DFGSSALoweringPhase (the 25th phase):
1 | 224:<!0:-> CheckInBounds(Int32:@69, Check:KnownInt32:@256, JS|MustGen|PureInt, Int32, Exits, bc#54, ExitValid) |
And @224 CheckInBounds is then changed to @137 CheckInBounds during some modification on index of graph in DFGObjectAllocationSinkingPhase(DFG phase object allocation elimination):
1 | 137:<!1:-> CheckInBounds(Int32:@69, KnownInt32:Kill:@159, JS|MustGen|PureInt, Int32, Exits, bc#54, ExitValid) |
vuln | fixed | |
---|---|---|
25th phase: DFGSSALoweringPhase | 224:<!0:-> CheckInBounds(Int32:@69, Check:KnownInt32:@256, MustGen, Int32, Exits, bc#54, ExitValid) |
224:<!0:-> CheckInBounds(Int32:@69, Check:KnownInt32:@256, JSMustGenPureInt, Int32, Exits, bc#54, ExitValid) |
70:<!0:-> GetByVal(KnownCell:@303, Int32:@69, Check:Untyped:@67, JSMustGen VarArgs PureInt, Int32, Int32+Array+InBounds+AsIs+Read, R:Butterfly_publicLength,IndexedInt32Properties, Exits, bc#54, ExitValid) predicting Other |
70:<!0:-> GetByVal(KnownCell:@303, Int32:@69, Check:Untyped:@67, **Check:Untyped:@224**, JS MustGen VarArgs PureInt, Int32, Int32+Array+InBounds+AsIs+Read, R:Butterfly_publicLength,IndexedInt32Properties, Exits, bc#54, ExitValid) predicting Other |
|
36th phase: DFGObjectAlloactionSinkingPhase | 137:<!0:-> CheckInBounds(Int32:@69, KnownInt32:Kill:@159, MustGen, Int32, Exits, bc#54, ExitValid) |
137:<!1:-> CheckInBounds(Int32:@69, KnownInt32:Kill:@159, JS MustGen PureInt, Int32, Exits, bc#54, ExitValid) |
70:<!1:-> GetByVal(KnownCell:@22, Int32:@69, Check:Untyped:Kill:@4, JS MustGen VarArgs PureInt, Int32, Int32+Array+InBounds+AsIs+Read, R:Butterfly_publicLength,IndexedInt32Properties, Exits, bc#54, ExitValid) predicting Other |
70:<!1:-> GetByVal(KnownCell:@22, Int32:@69, Check:Untyped:Kill:@4, **Check:Untyped:Kill:@137**, JS MustGen VarArgs PureInt, Int32, Int32+Array+InBounds+AsIs+Read, R:Butterfly_publicLength,IndexedInt32Properties, Exits, bc#54, ExitValid) predicting Other |